Borland Online And The Cobb Group Present:


February, 1995 - Vol. 2 No. 2

C++ Programming Basics - Implementing reference counting with smart pointers

Recently, we've looked at some of the benefits of using smart pointers in C++ programs ("Automatically Deallocating Memory Using Smart Pointers" [September 1994] and "Creating a Smart Pointer Template Class" [October 1994]). As we've seen, smart pointers simplify many aspects of managing heap objects.

However, the smart pointers we've examined so far assume that there's only one smart pointer for a given heap object. If you initialize two simple smart pointers (like the ones we've shown) with one heap object, the first smart pointer will delete the heap object when you destroy it. From that point, the second smart pointer won't address a valid object, and it will then try to delete the heap object again from the second smart pointer's destructor (assuming the program hasn't crashed by then).

In this article, we'll show how you can avoid this problem by implementing reference counting in your smart pointer classes. Reference counting is a technique in which a smart pointer class checks to see if it's the last smart pointer referencing a heap object before it deletes the heap object.

Who's counting?

Before we further describe reference counting, let's briefly define smart pointer. A smart pointer is a stack-based object that controls your interaction with a heap-based object. To create a smart pointer, you create a class that defines­­for stack-based objects­­the operations you'd normally perform via a pointer.

A smart pointer that implements reference counting has three major tasks to perform: incrementing the reference count when you initialize a new smart pointer object, decrementing the reference count when you destroy a smart pointer object, and deleting the heap object when the reference count reaches zero. (The reference count will be zero when the last smart pointer for the heap object leaves the current scope.)

Each of these three tasks requires access to the reference count for a heap object. However, you'll need to decide where you're going to store the reference count. If you decide to store the reference count with the heap object, you have three options: create a template "wrapper" class for the heap objects, hide the count on the heap with a custom operator new( ) function, or create a base class and derive your heap object classes from it. For a full description of these techniques, their benefits, and their drawbacks, BCJ - Other ways to perform reference counting.

If you'd rather keep the reference count for a given heap object in the smart pointer, you'll need to do a little more work. (This is because the reference count needs to be accurate, as the program creates and destroys smart pointers.)

If you want to make the class useful for real programs, you'll want your reference- counting scheme to accommodate smart pointers for multiple heap objects. To maintain the reference counts this way, you'll need to create two static arrays for the smart pointer class.

The first array will contain pointers to the heap objects that the smart pointers manage, and the second array will contain the corresponding reference counts for the pointers in the first array, as illustrated in Figure A. If you implement these arrays using Borland's template class TArrayAsVector, you'll be able to simplify the process of adding or deleting a new heap object/reference count combination to the arrays.


Figure A - The smart pointer class can count references by maintaining an array of pointers and an array of reference counts.

The most significant advantage to keeping the reference counts in the smart pointer class is that you can use pointers to existing class objects. Therefore, you don't need to wrap the objects in a template class, provide a custom operator new( ) function, or create a special class for reference-counted objects.

Reference counter

Now, let's create a simple example program that implements reference counting by storing the reference counts in the smart pointer class. To begin, launch the Borland C++ 4.0 Integrated Development Environment (IDE).

When the IDE's main window appears, choose New Project... from the Project menu. When the New Project dialog box appears, enter \REFCNTR\REFCNTR.IDE in the Project Path And Name entry field, select EasyWin in the Target Type list box, select Small in the Target Model combo box, and then click the Class Library check box in the Standard Libraries section.

Next, click Advanced... to display the Advanced Options dialog box. Select the CPP Node radio button, select the DEF check box, and then click OK. When the New Project dialog box reappears, click OK to create the project.

When the IDE finishes creating the project, the REFCNTR.IDE project window will appear. In this window, right-click on the name refcntr [.def] and choose Edit Node Attributes... from the pop-up menu. In the Node Attributes dialog box, enter \BC4\LIB\DEFAULT.DEF in the Name entry field and click OK.

Now, choose New from the File menu. When the new editor window appears, enter the code from Listing A.


Listing A: smartptr.h

#if !defined( __CLASSLIB_ARRAYS_H )
#include <\classlib\arrays.h>
#endif

template<class T> class SmartPtr
{ T* ptr;
  static TArrayAsVector<long> refCountArray;
  static TArrayAsVector<T*> pointerArray;
  static const char* className;

  static void AddRef(T* newPtr)
  { int ptrIdx;
    ptrIdx = pointerArray.Find(newPtr);
    if(ptrIdx != INT_MAX)
    { ++(refCountArray[ptrIdx]); }
    else
    { TRACE("Registering " \
       << (void*)newPtr);
      pointerArray.Add(newPtr);
      refCountArray.Add(1);
    }
  }

  static void DelRef(T* currentPtr)
  { int ptrIdx = pointerArray.Find(currentPtr);
    if(ptrIdx != INT_MAX)
    { -(refCountArray[ptrIdx]);
      if(refCountArray[ptrIdx] < 1)
      { TRACE("Deleting " \
         << (void*)pointerArray[ptrIdx]);
        pointerArray.Destroy(ptrIdx);
        refCountArray.Destroy(ptrIdx);
      }
    }
    else
      TRACE("Couldn't find pointer in array");
  }

 public:
  SmartPtr(T* p) : ptr(p)
  { AddRef(ptr); }

  ~SmartPtr( )
  { DelRef(ptr); }

  operator T*( ) const
  { return ptr; }

  operator const T*( ) const
  { return ptr; }

  T* operator-> ( )
  { return ptr; }

  SmartPtr<T>& operator= (const SmartPtr<T>& source)
  { DelRef(ptr);
    ptr = source.operator T*( );
    AddRef(ptr);
    return *this;
  }

  SmartPtr(const SmartPtr<T>& source)
  { ptr = source.operator T*( );
    AddRef(ptr);
  }

  static void printSummary( );
};

template <class T>
void SmartPtr<T>::printSummary( )
{
  TRACE("** Reference Count Summary **");
  int count = pointerArray.GetItemsInContainer( );
  for(int ct = 0; ct < count; ++ct)
  {
    TRACE("  " << className << " object @" \
     << (void *)pointerArray[ct] \
     << " - Ref Count = " << refCountArray[ct]);
  }
  TRACE("*****************************");
}
#define INIT_SMART_PTR_STATICS(T) \
 const char* SmartPtr<T>::className = "<"#T">";\
 TArrayAsVector<long> SmartPtr<T>::refCountArray = \
   TArrayAsVector<long>(5,0,5);\
 TArrayAsVector<T*> SmartPtr<T>::pointerArray = \
   TArrayAsVector<T*>(5,0,5);

When you finish entering the code, choose Save As... from the File menu. In the Save File As dialog box, enter \SMARTPTR.H in the File Name entry field and click OK.

Double-click on the refcntr [.cpp] node in the project window. When the editor window for this file appears, enter the code from Listing B. When you finish entering this code, choose Save from the File menu.


Listing B - REFCNTR.CPP

#include "smartptr.h"

struct Money
{
  Money(long d, long c) :
    dollars(d), cents(c) {}
  long dollars;
  long cents;
};

INIT_SMART_PTR_STATICS(Money);

typedef SmartPtr<Money> SmartMoneyPtr;

void foo(Money* newMoney)
{
  TRACE("Entering foo( )");
  SmartMoneyPtr::printSummary( );

  // Create a smart pointer for
  // the parameter heap object pointer
  SmartMoneyPtr
    fooMoney(newMoney);

  // Create a new smart pointer
  // and heap object
  SmartMoneyPtr
    tempMoney(new Money(3, 35));

  SmartMoneyPtr::printSummary( );

  TRACE("Abandon pointer!");
  tempMoney = fooMoney;

  SmartMoneyPtr::printSummary( );
  TRACE("Leaving foo( )");
}

int main( )
{
  SmartMoneyPtr::printSummary( );

  // Create a heap object & pointer
  Money* cash =
    new Money(1, 12);
  // Create a heap object & smart pointer
  SmartMoneyPtr
    twoBitObject(new Money(0, 25));

  SmartMoneyPtr::printSummary( );

  TRACE("Calling Copy Constructor");
  SmartMoneyPtr quarter(twoBitObject);

  SmartMoneyPtr::printSummary( );

  // Create a smart pointer for
  // an existing heap object pointer
  SmartMoneyPtr
    smartCash(cash);

  SmartMoneyPtr::printSummary( );

  TRACE("Calling operator=");
  twoBitObject = smartCash;

  SmartMoneyPtr::printSummary( );

  // Pass a smart pointer to a function
  // that expects a heap pointer
  foo(twoBitObject);

  SmartMoneyPtr::printSummary( );
  return 0;
}
List Test thinger to see if this can be recorded!!!!!


Finally, right-click on refcntr [.exe] and choose Edit Node Attributes... from the pop-up menu. In the Node Attributes dialog box, choose Diagnostics from the Style Sheet combo box, and then click OK.

Choose Event Log from the View menu to display the Event Log window. (You'll be able to view the TRACE messages in this window.) Then, choose Run from the Debug menu to build and run the program.

By the way, the first time you run this program, you may see a Cannot Write to AUX Device error message. If so, click Cancel and ignore the message.

When the program runs, it won't display the standard EasyWin screen because the program didn't create any direct output. Instead, you'll see a list of messages in the Event Log window. Now, let's step through the code for this program to see how it produces this output.

Counter intelligence

At the center of this reference-counting scheme are two arrays that are static members of the smartPtr class: refCountArray and pointerArray. The refCountArray member is a dynamically sized TArrayAsVector object that holds the actual reference counts as long int values. The pointerArray member is a similar TArrayAsVector object that holds the heap object pointers.

When you create a smartPtr object, the constructor calls the private AddRef( ) static member function. This function searches the array of heap pointers to see if the newPtr pointer is already there. If the pointer exists in the pointer array, the function simply increments the reference-count value that corresponds to the newPtr pointer.

If the newPtr pointer isn't in the pointer array, the AddRef( ) function displays a registration message with the TRACE( ) diagnostic macro. Then, the AddRef( ) function adds the pointer to the pointer array and adds a reference count of 1 to the refCountArray member.

When the smartPtr object's destructor runs, it calls the private DelRef( ) static member function. The DelRef( ) function determines the position of the object's pointer and then decrements the corresponding reference count. If the count reaches zero, the function displays a deletion message with the TRACE( ) diagnostic macro, and then it destroys the heap object and the reference-count element for that object.

The remainder of the class is fairly easy to understand. However, at the end of the SMARTPTR.H file, you'll notice that we've declared the INIT_SMART_PTR_STATICS macro to initialize the two static arrays and a char pointer that addresses the type name of the heap objects the smart pointers manage. If you use the smartPtr class, you'll need to include a call to this macro for each smart pointer type.

A foo and its Money

Now let's quickly review how the REFCNTR program uses the SmartPtr class. As you look through the source code, you'll be able to trace the program's execution by referring to Figure B, which is the output that appears in the Event Log window.


Figure B - When you run the REFCNTR.EXE program, you'll see these TRACE messages in the Event Log window.

** Reference Count Summary **  
*****************************  
Registering 0x3cdc  
** Reference Count Summary **  
  <Money> object @0x3cdc - Ref Count = 1  
*****************************  
Calling Copy Constructor  
** Reference Count Summary **  
  <Money> object @0x3cdc - Ref Count = 2  
*****************************  
Registering 0x3cd0  
** Reference Count Summary **  
  <Money> object @0x3cdc - Ref Count = 2  
  <Money> object @0x3cd0 - Ref Count = 1  
*****************************  
Calling operator=  
** Reference Count Summary **  
  <Money> object @0x3cdc - Ref Count = 1  
  <Money> object @0x3cd0 - Ref Count = 2  
*****************************  
Entering foo( )  
** Reference Count Summary **  
  <Money> object @0x3cdc - Ref Count = 1  
  <Money> object @0x3cd0 - Ref Count = 2  
*****************************  
Registering 0x3ce8  
** Reference Count Summary **  
  <Money> object @0x3cdc - Ref Count = 1  
  <Money> object @0x3cd0 - Ref Count = 3  
  <Money> object @0x3ce8 - Ref Count = 1  
*****************************  
Abandon pointer!  
Deleting 0x3ce8  
** Reference Count Summary **  
  <Money> object @0x3cdc - Ref Count = 1  
  <Money> object @0x3cd0 - Ref Count = 4  
*****************************  
Leaving foo( )  
** Reference Count Summary **  
  <Money> object @0x3cdc - Ref Count = 1  
  <Money> object @0x3cd0 - Ref Count = 2  
*****************************  
Deleting 0x3cdc  
Deleting 0x3cd0

At the beginning of the source file REFCNTR.CPP, we declare the Money struct. There's nothing special about this struct in relation to our SmartPtr class­­we just created it to show how the SmartPtr class can work with any type of object that you dynamically allocate.

Immediately below the Money declaration, you'll find the line

INIT_SMART_PTR_STATICS(Money);

We call this macro to initialize the static arrays in the SmartPtr class. Since we're passing the Money class name to the macro, the macro is able to set the static variables for the SmartPtr<Money> template class.

Below the line that calls the macro, you'll find a typedef statement that allows us to use the name SmartMoneyPtr instead of SmartPtr<Money>. (This makes the code easier to read.)

For now, skip the foo( ) function and examine the main( ) function instead. The first statement in the main( ) function is a call to the printSummary( ) function from the SmartMoneyPtr class. (Remember, this is really the SmartPtr<Money> template class.)

Since the printSummary( ) function is a static function, you can call it using the name of the class instead of calling it using the SmartMoneyPtr object's identifier. The purpose of this function is simply to use the TRACE diagnostic macro to display the heap objects and reference counts that the class is currently storing in its static arrays. As the program runs, we call this function repeatedly to allow you to see how the contents of the arrays change as the program creates and destroys smart pointer objects.

In the remainder of the program, we create SmartMoneyPtr objects in various ways to demonstrate the flexibility you'll have when you maintain the reference counts in the smart pointer class. (Reference-counting schemes that store the count with the heap object won't have this kind of flexibility.)

If you look at the foo( ) function, you'll see a particularly difficult scenario. Near the end of the main( ) function, we pass the twoBitObject variable to the foo( ) function. Since the function expects a Money* parameter, the compiler will call the SmartMoneyPtr class's conversion operator to convert the smart pointer to a Money* value.

Inside the foo( ) function, we create a new SmartMoneyPtr object named fooMoney and initialize it with the Money* function parameter newMoney. Then, we create another SmartMoneyPtr (initializing it with a new heap object) and abandon that heap object by assigning the fooMoney smart pointer to the tempMoney smart pointer. (If we weren't using smart pointers, this would create a definite memory leak.)

As you refer to Figure B, notice that the SmartMoneyPtr class deletes each heap object when there's no longer a smart pointer referring to that heap object. This is proof that our reference-counting technique works.

Check your references

If you use the SmartPtr class in your programs, you'll want to avoid two potential problems. The first problem can occur if you create smart pointer classes for related types.

For example, if you declare a class named Base and then derive a new class named Derived, the compiler will allow you to write the following code:

Derived* derivedObject = new Derived( );

SmartPtr<Derived> derivedPtr(derivedObject);
SmartPtr<Base> basePtr(derivedObject);

Now, the SmartPtr<Derived> class's static array of heap objects will contain the address of the derivedObject heap object and a reference count of 1. Unfortunately, the SmartPtr<Base> class's static array will also contain this object's address and a separate reference count of 1. (This happens because template classes­­even those whose template parameter types are related­­have no relationship with each other.)

When the compiler destroys the derivedPtr object, it will delete the derivedObject heap object because its reference count will be 0. When the compiler destroys the basePtr object, it will try to delete the heap object a second time. Unfortunately, the C++ language doesn't define what happens when you delete a heap object twice.

To avoid this problem completely, create smart pointers only for the base classes in your program. If you must create smart pointers for derived classes (to call derived-class member functions), don't initialize different types of smart pointers with the same heap object.

The second problem can occur if you use the SmartPtr class in a project that has multiple source files. If you create smart pointer objects in different source files for the same type of heap object, you may see symbol redefinition errors if you're using the -Jgd compiler switch. (This generates global definitions of template class instances.)

Conclusion

Smart pointers can make it easy to manage deallocating the memory for heap objects. If you use the technique we've shown here to add reference counting to your smart pointer class, you'll be able to safely initialize more than one smart pointer to any individual heap object.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.